-
Notifications
You must be signed in to change notification settings - Fork 272
Track SessionContext instead of InvocationContext #4916
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
1. Since the shift from displaying one trigger at a time to displaying multiple triggers at a time, use SessionContext as the disposable for the session states. 2. Introduce the concept of "display session" which can show multiple triggers at a time and sessionContext is to hold states for that session. 3. ongoingRequests will be a map of <jobId, InvocationContext> where InvocationContext is the state of each trigger. 4. Managed these states globally by dynamically adding Trigger states into ongoingRequests and whenever the user made a decision(accept or reject), conclude decisions for all the ongoing triggers and clear ongoingRequest, so that a new display session can start later.
...y/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt
Outdated
Show resolved
Hide resolved
private var refreshFailure: Int = 0 | ||
private val ongoingRequests = mutableMapOf<Int, InvocationContext?>() | ||
val ongoingRequestsContext = mutableMapOf<Int, RequestContext>() | ||
private var jobId = 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this overflow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a monthly trigger limit which is 25000, so jobId will increment 25000 each month if they don't restart IDE.
private val ongoingRequests = mutableMapOf<Int, InvocationContext?>() | ||
val ongoingRequestsContext = mutableMapOf<Int, RequestContext>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if this is the best pattern if the exact jobId is only relevant for "which is the last request", which we can determine in other ways
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jobId is only to separate different triggers and is a local identifier only, (same as sessionId except we know the jobId as soon as a trigger happens before receiving the response)
return | ||
} | ||
val caretContext = requestContext.fileContextInfo.caretContext | ||
ongoingRequestsContext.forEach { (k, v) -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for example, this is guaranteed to iterate in-order from first request inserted in the map to last
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea, the purpose is not just to identify which the last request is -- but more to separate states of different triggers.
Later on when we send UserTriggerDecisions we will need to send one for each jobId(trigger), having a jobId here and logging it keeps track of what happened E2E for this specific trigger.
// It's possible and ok that coroutine will keep running until the next time we check it's state. | ||
// As long as we don't show to the user extra info we are good. | ||
val coroutineScope = disposableCoroutineScope(popup) | ||
val coroutineScope = projectCoroutineScope(requestContext.project) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CodeWhispererService
has a service-level scope that should be used instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
|
||
data class RecommendationContext( | ||
val details: List<DetailContext>, | ||
val details: MutableList<DetailContext>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not seeing why need mutable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now each trigger will only have one state InvocationContext
and is stored in ongoingRequests
, when subsequent suggestions sent back to client, they will be added to this mutable details, and other parts of InvocationContext
remains the same.
private fun updateStates(
states: InvocationContext,
response: GenerateCompletionsResponse
): InvocationContext {
val recommendationContext = states.recommendationContext
val newDetailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext(
states.requestContext,
recommendationContext.userInputSinceInvocation,
response.completions(),
response.responseMetadata().requestId()
)
recommendationContext.details.addAll(newDetailContexts)
return states
}
otherwise we will have to replace InvocationContext
which I feel is a bit unnecessary.
if (hasAccepted) { | ||
popup?.closeOk(null) | ||
} else { | ||
popup?.cancel() | ||
} | ||
popup?.let { Disposer.dispose(it) } | ||
popup = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
popup manager and sessioncontext are so tied together that it feels like it should just be handled together
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
popup now becomes a state of a session (1 session 1 popup) so I put it as a field of SessionContext
, and then I let disposing SessionContext handles everything so I don't have to call
CWService.getInstance().disposeDisplaySession()
popup.cancel()/close()
separately.
it.closeOk(null) | ||
Disposer.dispose(it) | ||
fun showPopup(sessionContext: SessionContext, force: Boolean = false) { | ||
val p = sessionContext.editor.offsetToXY(sessionContext.popupDisplayOffset) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
popupOffset
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
private fun getValidCount(): Int = | ||
CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo().filter { isValidRecommendation(it) }.size | ||
|
||
private fun getValidSelectedIndex(selectedIndex: Int): Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feels like this could be cleaner but i see that it's the same as the old logic
can we add some doc strings for different models |
val jobId: Int, | ||
val detail: DetailContext, | ||
val userInput: String, | ||
val typeahead: String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dumb q, what's the difference between these 2 typeaheads
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
previously we have typeahead and typeaheadoriginal for the purpose of accounting leading spaces in the typeahead,
for example, typeaheadoriginal will be System
and typeahead will be System
, and then we use typeahead to do the suggestion matching logic. This is to preserve the suggestions as much as possible.
Now we trigger more so we have more display oppotunities, I feel like this becomes a bit unnecessary and makes the logic complicated, so remove typeahead original to make it no longer accomodate leading spaces.
private val ongoingRequests = mutableMapOf<Int, InvocationContext?>() | ||
val ongoingRequestsContext = mutableMapOf<Int, RequestContext>() | ||
private var jobId = 0 | ||
private var sessionContext: SessionContext? = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure do we need this to be thread safe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically it needs, so the current rule is to modify this only in EDT so I believe in various places I have @RequiresEDT
annotation including disposeDisplaySession
, and initialing SessionContext also is in EDT.
class CodeWhispererService(private val cs: CoroutineScope) : Disposable { | ||
private val codeInsightSettingsFacade = CodeInsightsSettingsFacade() | ||
private var refreshFailure: Int = 0 | ||
private val ongoingRequests = mutableMapOf<Int, InvocationContext?>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the case when InvocationContext being null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
before the InvocationContext for the specific jobId is added, it can be null, we did some null check to know if it's first request/response.
processCodeWhispererUI( | ||
sessionContext, | ||
it, | ||
ongoingRequests[currentJobId], | ||
cs, | ||
currentJobId | ||
) | ||
if (!ongoingRequests.contains(currentJobId)) { | ||
cs.coroutineContext.cancel() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
andrew could you help me understand why isn't this in the format of
if (ongoingRequests.contains(currentJobId)) {
processCodeWhispererUi()
}
but execute processCodeWhispererUi and cancel the coroutine later, what's the expected behavior here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so intially there's no InvocationContext in ongoingRequests, and then we contruct it in initState
in processCodeWhispererUi
and put it into the map. If after this function it's still null, then it means state creation failed due to e.g. all discarded suggestions or all empty suggestions.
ongoingRequests[currentJobId], | ||
cs, | ||
currentJobId | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why here doesn't need the
if (!ongoingRequests.contains(currentJobId)) {
cs.coroutineContext.cancel()
}
check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it still needs, I will add it in the following PR.
...y/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt
Show resolved
Hide resolved
sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0] | ||
displayMessage = e.awsErrorDetails().errorMessage() ?: message("codewhisperer.trigger.error.server_side") | ||
} else if (e is software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException) { | ||
} else if (e is CodeWhispererRuntimeException) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the change!
...tware/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererInvocationStatus.kt
Show resolved
Hide resolved
val project: Project, | ||
val editor: Editor, | ||
var popup: JBPopup? = null, | ||
var selectedIndex: Int = -1, | ||
val seen: MutableSet<Int> = mutableSetOf(), | ||
val isFirstTimeShowingPopup: Boolean = true, | ||
var isFirstTimeShowingPopup: Boolean = true, | ||
var toBeRemovedHighlighter: RangeHighlighter? = null, | ||
var insertEndOffset: Int = -1 | ||
) | ||
var insertEndOffset: Int = -1, | ||
var popupOffset: Int = -1, | ||
val latencyContext: LatencyContext, | ||
var hasAccepted: Boolean = false | ||
) : Disposable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some of these can be private / don't need to be initialized in the primary constructor since the initial value is always expected to be the same
Types of changes
Description
Checklist
License
I confirm that my contribution is made under the terms of the Apache 2.0 license.